Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: dynamic data in defineRouteMeta #3096

Open
wants to merge 3 commits into
base: v2
Choose a base branch
from

Conversation

horvbalint
Copy link
Contributor

@horvbalint horvbalint commented Feb 15, 2025

πŸ”— Linked issue

#2974

❓ Type of change

  • πŸ“– Documentation (updates to the documentation, readme, or JSdoc annotations)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • πŸ‘Œ Enhancement (improving an existing functionality like performance)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

πŸ“š Description

In this PR I tried to add a very basic implementation of dynamic data in defineRouteMeta. It does not evaluate the meta in build time so if an approach like this is not desired, feel free to close this PR :)

The basic idea is that since we are already creating a virtual file that exports the route meta object for every route, it would make sense to have the computation in these files as well. I am searching through the route file code until the macro invocation is found. Along the way relative imports are updated with their absolute path. If the macro is found, I discard the lines after it and replace it with a default export of its argument.

As an example this route:

import utilityFunction from './utilityPath'
const openAPI = { description: 'The routes description' }

defineRouteMeta({ openAPI })

export default defineEventHandler(() => utilityFunction())

is transformed into this virtual file:

import utilityFunction from 'absolute/path/utilityPath'
const openAPI = { description: 'The routes description' }

export default { openAPI }

Though this is a very simple idea, it is working surprisingly well, allowing us to do very nice things with it like:

import { z } from 'zod'
import { extendZodWithOpenApi, createSchema } from 'zod-openapi'

extendZodWithOpenApi(z)

const bodyValidator = z.object({
    foo: z.enum(["bar", "baz"]).openapi({ description: "Property description" }),
})
const { schema, components } = createSchema(bodyValidator);

defineRouteMeta({
  description: routeDescription,
  requestBody: {
    content: {
      "application/json": {
        schema,
      },
    },
  },
  $global: {
    components,
  },
});

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, validator.parse);
});

The meta or parts of it can also come from imports, so we can create utilities, like:

// server/utils/openapi.ts

import { z } from "zod";
import { extendZodWithOpenApi, createSchema } from "zod-openapi";

extendZodWithOpenApi(z);

export function createValidator(routeDescription: string, callback: (zod: typeof z) => z.AnyZodObject) {
  const validator = callback(z);
  const { schema, components } = createSchema(validator);

  return {
    validator,
    openAPI: {
      description: routeDescription,
      requestBody: {
        content: {
          "application/json": {
            schema,
          },
        },
      },
      $global: {
        components,
      },
    },
  };
}
// server/routes/index.post.ts

import { createValidator } from "~/utils/openapi";

const { validator, openAPI } = createValidator("Route description", (z) =>
  z.object({
    foo: z.enum(["bar", "baz"]).openapi({ description: "Property description" }),
  })
);

defineRouteMeta({ openAPI });

export default defineEventHandler(async (event) => {
  const body = await readValidatedBody(event, validator.parse);
});

Of course there is no shortage of drawbacks/todos either:

  • Relative imports can not be used in the macro
  • Every code above the macro runs twice
    • this sounds bad, but users can be encouraged to place the macro as far up in the file as possible. In nuxt they can only appear at the very top
  • If the code above the macro has side-effects, those will happen multiple times
    • I don't think this can be prevented in any way
  • Variables placed under the macro can not be used in it, even if they are usually hoisted (functions, vars)

πŸ“ Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@horvbalint
Copy link
Contributor Author

horvbalint commented Feb 16, 2025

I had to add acorn as a dependency and use its parse method, because rollup's this.parse had some type mismatches with reality (start and end was not present in it). I am not sure however what the correct parse options would be. Other than that this appeats to be working fine.

@horvbalint horvbalint marked this pull request as ready for review February 16, 2025 08:42
@horvbalint horvbalint requested a review from pi0 as a code owner February 16, 2025 08:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant